/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file opengl.cpp
 * @author TooOld2Rock'nRoll
 * @see opengl.hpp
 */
/*---- Includes ----*/
#include "graphics/opengl.hpp"

#include <stdexcept>
#include <fmt/core.h>

#include "toolbox/debug.h"


/*---- Global Variables ----*/


/*---- Local Functions ----*/
//it will only do something if ENABLE_OPENGL_DEBUG is true.
static void GLAPIENTRY GlDebugMessageCallback (GLenum source, GLenum type, GLuint id, GLenum severity,
                                                 GLsizei length, const GLchar* message, const void* userParam)
{
    unsigned short source_i = 6;
    static const char *source_names[] = {"API", "APPLICATION", "OTHER", "SHADER_COMPILER",
                                         "THIRD_PARTY", "WINDOW_SYSTEM", "UNKNOWN"};

    unsigned short type_i = 9;
    static const char *type_names[] = {"GL_DEPRECATED", "GL_ERROR", "GL_MARKER", "GL_OTHER", "GL_PERFORMANCE",
                                       "GL_POP_GROUP", "GL_PORTABILITY", "GL_PUSH_GROUP", "GL_UNDEFINED_BEHAVIOR",
                                       "GL_UNKNOWN"};

    unsigned short severity_i = 4;
    static const char *severity_names[] = {"LOW", "MEDIUM", "HIGH", "NOTIFICATION", "UNKNOWN"};

    if (DEBUG_OPENGL_LEVEL > severity)
        return;//filter debug output

    switch (severity)
    {
        case GL_DEBUG_SEVERITY_HIGH: severity_i = 2; break;

        case GL_DEBUG_SEVERITY_MEDIUM: severity_i = 1; break;

        case GL_DEBUG_SEVERITY_LOW: severity_i = 0; break;

        case GL_DEBUG_SEVERITY_NOTIFICATION: severity_i = 3; break;

        default: break;
    }//end switch (severity)

    switch (source)
    {
        case GL_DEBUG_SOURCE_API: source_i = 0; break;

        case GL_DEBUG_SOURCE_APPLICATION: source_i = 1; break;

        case GL_DEBUG_SOURCE_OTHER: source_i = 2; break;

        case GL_DEBUG_SOURCE_SHADER_COMPILER: source_i = 3; break;

        case GL_DEBUG_SOURCE_THIRD_PARTY: source_i = 4; break;

        case GL_DEBUG_SOURCE_WINDOW_SYSTEM: source_i = 5; break;

        default: break;
    }//end switch (source)

    switch (type)
    {
        case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: type_i = 0; break;

        case GL_DEBUG_TYPE_ERROR: type_i = 1; break;

        case GL_DEBUG_TYPE_MARKER: type_i = 2; break;

        case GL_DEBUG_TYPE_OTHER: type_i = 3; break;

        case GL_DEBUG_TYPE_PERFORMANCE: type_i = 4; break;

        case GL_DEBUG_TYPE_POP_GROUP: type_i = 5; break;

        case GL_DEBUG_TYPE_PORTABILITY: type_i = 6; break;

        case GL_DEBUG_TYPE_PUSH_GROUP: type_i = 7; break;

        case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: type_i = 8; break;

        default: break;
    }//end switch (type)

    printf ("%s - %s: type = %s, severity = %s, message = %s\n",
            __FUNCTION__, source_names[source_i], type_names[type_i], severity_names[severity_i], message);
}//end GlDebugMessageCallback ()


/*---- Methods Implementation ----*/
OpenGl::OpenGl ()
{
    _debug (INFO, "::%s () <--The OpenGl singleton has been created!", __FUNCTION__);
}//end Constructor

/**
 * @brief Access the singleton instance.
 *
 * @return Reference to the OpenGl singleton.
 */
OpenGl& OpenGl::instance ()
{
    static OpenGl _instance;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    return _instance;
}//End instance ()

OpenGl::~OpenGl ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);
    _debug (INFO, "::%s () <--The OpenGl singleton will be destroyed!", __FUNCTION__);

    if (this->glMainContext != nullptr)
        SDL_GL_DeleteContext (this->glMainContext);
    if (this->glThreadContext != nullptr)
        SDL_GL_DeleteContext (this->glThreadContext);
}//End Destructor


/**
 * @brief Setup the necessary links to make the API work and create the objects to keep it running.
 *
 * @param sdl_win - a pointer to the native window manager.
 */
void OpenGl::init (SDL_Window *sdl_win)
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (!sdl_win)
        throw std::invalid_argument (fmt::format ("{0}:{1} - no SDL Window reference provided!", __LINE__, __FILE__));
    if (this->p_sdlWindow)
    {
        _debug (ERROR, "OpenGl has already been setup, don't call init() twice!");
        return;
    }//end if (this->p_sdlWindow)

    this->p_sdlWindow = sdl_win;
    this->glThreadContext = SDL_GL_CreateContext (sdl_win);
    this->glMainContext = SDL_GL_CreateContext (sdl_win);

    _debug (INFO, "OpenGl context created successfully.");

    // glad: load all OpenGL function pointers
    if (!gladLoadGL ((GLADloadfunc) SDL_GL_GetProcAddress))
        throw std::runtime_error ("Glad failed to load OpenGl addresses! Check the OpenGl Core version.");
    _debug (INFO, "OpenGl interfaces loaded successfully.");

    //it will only do something if ENABLE_OPENGL_DEBUG is true.
    _debug_enableGlMessages (GlDebugMessageCallback);

    //how to handle transparency
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    _debug (INFO, "Transparency set to Alpha channel...");

    //enable anti-aliasing
    glEnable (GL_MULTISAMPLE);
    _debug (INFO, "OpenGl is using anti-aliasing...");

    //when clearing the screen, we will use black!
    glClearColor (0.f, 0.f, 0.f, 1.f);
    _debug (INFO, "Background color (clear screen) is set to Black...");

    //keep up to environment changes that affect OpenGl behavior.
    this->p_ev_mgr_instance = &EventManager::instance();
    this->id_high_ev_listener = this->p_ev_mgr_instance->addListener ((EventManager::EventListenerCallback)this->highPriorityEvents_cb, this);

    this->p_ev_mgr_instance->listenForEvent (this->id_high_ev_listener, SDL_WindowEventID::SDL_WINDOWEVENT_SIZE_CHANGED);
}//End init ()


/**
 * @brief Locks the access to the background/working thread Gl pipeline.
 * @remarks In practice, works like a traditional mutex, call it at the beginning of the protected section.
 *
 * @return The sync reference required to unlock the thread at the end of the procedures.
 */
GLsync OpenGl::lockBackgroundContext ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //activate the OpenGl background context
    SDL_GL_MakeCurrent (this->p_sdlWindow, this->glThreadContext);
    return glFenceSync (GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}//End lockBackgroundContext ()


/**
 * @brief Unlocks the background/working thread to be used by other procedures.
 *
 * @param lock - the sync object returned by lockBackgroundContext() method.
 */
void OpenGl::unlockBackgroundContext (GLsync lock)
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //make sure the OpenGl context is stable and ready before proceeding
    while (glClientWaitSync (lock, GL_SYNC_FLUSH_COMMANDS_BIT, GLuint64(5000000)) == GL_TIMEOUT_EXPIRED)
        _debug (INFO, "Gl sync timed out waiting for background context release, retrying...");
    SDL_GL_MakeCurrent (this->p_sdlWindow, NULL);
}//End clearBackgroundContext ()


/**
 * @brief Process high priority events pertinent to OpenGl.
 *
 * @param obj_this - pointer to the OpenGl instance.
 * @param ev - the event that was triggered.
 */
void OpenGl::highPriorityEvents_cb (OpenGl *obj_this, const EventManager::event_s &ev)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    switch (ev.type)
    {
        case EventManager::event_type_e::WINDOW:
        {
            static window_event_info_s *win_ev = nullptr;

            win_ev = (window_event_info_s *)ev.p_data;

            ///@todo: change to a switch when we have more cases!! https://wiki.libsdl.org/SDL2/SDL_WindowEvent
            if (win_ev->window_ev_id == SDL_WINDOWEVENT_SIZE_CHANGED)
                glViewport (0, 0, win_ev->i_data1, win_ev->i_data2);

            break;
        }//end case EventManager::event_type::WINDOW:

        default:
            //something not expected, ignore it.
            _debug (WARNING, "Unexpected event -> %u", ev.type);
            break;
    }//end switch (ev._type)
}//End highPriorityEvents_cb ()


/**
 * @brief Constructor.
 *
 * @param vert_size - value returned by sizeof() for the vertices array.
 * @param vertices[] - the vertices array with a list of points to use.
 * @param index_size - value returned by sizeof() for the indexes array.
 * @param indexes[] - the indexes array with the order of points to be used to form triangles.
 */
SharedGlObjs::SharedGlObjs (std::size_t vert_size, const GLfloat vertices[], std::size_t index_size, const GLuint indexes[])
{

    _debug (TRACE, "::%s ()", __FUNCTION__);

    glGenBuffers (1, &this->VBO);
    glBindBuffer (GL_ARRAY_BUFFER, this->VBO);
    glBufferData (GL_ARRAY_BUFFER, vert_size, vertices, GL_STATIC_DRAW);

    glGenBuffers (1, &this->EBO);
    glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->EBO);
    glBufferData (GL_ELEMENT_ARRAY_BUFFER, index_size, indexes, GL_STATIC_DRAW);
}//end Constructor


/**
 * @brief Destructor.
 */
SharedGlObjs::~SharedGlObjs ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    glDeleteBuffers (1, &this->EBO);
    glDeleteBuffers (1, &this->VBO);
}//end Destructor

